{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "c6b536a0-6c18-491c-8c46-df7bcf7642aa",
   "metadata": {},
   "source": [
    "## 0 课程规划\n",
    "\n",
    "欢迎来到《深度学习中的时间序列算法群》公开课。在这门课程中，我将带你从0认识5大类深度学习中的时间序列模型，并为你深度讲解深度时序算法众多的的精彩理念与实现方式。当你完成这门课程时，你将完成深度时序算法入门，打好进一步学习更多高级架构的基础。\n",
    "\n",
    "**DAY 1：LSTM与深度学习中的时间序列**\n",
    "1. 深度学习中的时间序列数据\n",
    "2. 时序数据 vs 非时序数据\n",
    "3. 循环神经网络如何处理时序问题\n",
    "4. LSTM的灵感起源与直觉理解\n",
    "5. LSTM的基本结构与架构设计\n",
    "\n",
    "**DAY 2：LSTM的参数全解与预测实战**\n",
    "1. PyTorch中的LSTM层与参数\n",
    "2. LSTM类的输入与输出\n",
    "3. 股价与时间序列数据预测实战\n",
    "\n",
    "**DAY 3：时序进阶：时序卷积网络TCN**\n",
    "1. 1d卷积操作与时序数据\n",
    "2. 因果卷积 Causal Convolution\n",
    "3. 1d膨胀卷积与感受野扩张\n",
    "4. 时序卷积网络的架构与实现\n",
    "5. 深度学习中的5大类时序解决方案\n",
    "\n",
    "**DAY 4：Transformer与Attention用于时间序列**\n",
    "\n",
    "**DAY 5：Informer架构解析与时序应用**\n",
    "\n",
    "**DAY 6：深度时序SOTA架构TabNet**\n",
    "\n",
    "更多后续课程请关注B站动态和小可爱私聊信息！"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2f9c3aab-28f3-4625-81db-8b9c8846d9ba",
   "metadata": {},
   "source": [
    "## 1 从循环网络到卷积网络"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "84557c0f-d62f-4f64-8de2-06eafefcd521",
   "metadata": {},
   "source": [
    "- **循环网络家族的问题**"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8da53a77-4d48-4c8b-a87a-8c3071303f0d",
   "metadata": {},
   "source": [
    "在前面的课程中，我们详细了解了深度学习中的时间序列数据以及循环神经网络家族算法，我们深度地解析了RNN、LSTM，并了解了循环网络处理时间序列问题的基本思路。我们了解到，时间序列数据之所以特别，是因为其信息量不止蕴含在特征之中，也蕴含在样本与样本之间。因此**在处理时间序列数据时，我们不仅要学习特征与标签之间的关系，还需要学习样本与样本之间的关系**。对时间序列数据而言，样本与样本之间的关系就是上一个时间点与下一个时间点之间的关系，因此**循环神经网络家族采取的手段是——依次处理时间点、并将上一个时间点的信息传递融入下一个时间点的信息的运算过程，使得下一个时间点能够获得上一个时间点传来的信息，从而在两个时间点中建立联系**。这是早年深度学习算法在处理时间序列数据时的一般思路。这一思路确实很巧妙，但是也存在着大量的问题——\n",
    "\n",
    "- **运算效率过低**。必须依次处理时间点意味着计算无法并行，因此RNN和LSTM的运算效率有极大的问题，在数据量巨大的深度学习领域，计算效率低下是致命的缺陷之一。\n",
    "\n",
    "- **简单的循环网络记忆机制不完善，有时遗忘太多，有时记住太多，难以处理长序列**。虽然循环网络家族建立起了时间点之间的联系，但这种联系有时不总是有效的，当一个时间点上包含的历史时间点信息越多，那些久远的历史时间点的信息就不太容易对当下的预测构成影响，比如RNN在遗忘方面的缺陷是十分显著的，浅层的lstm也容易在记忆上出现问题。\n",
    "\n",
    "- **复杂的循环网络虽然擅长记忆，但是运算流程过于复杂，参数量很大**。LSTM尝试在RNN的基础上将记忆机制改进，整体表现非常不错，但是LSTM的整体复杂程度却非常高，具有相同尺寸隐藏层的RNN的参数量只有LSTM的1/5不到。\n",
    "\n",
    "- **训练难，冲突问题和梯度问题难以被根治**。权重冲突、长短期记忆的冲突、梯度消失、梯度爆炸问题都是RNN和LSTM常见的训练问题，这些问题阻碍了循环网络在复杂任务上获得较好的表现。\n",
    "\n",
    "尽管循环神经网络已经有效地建立了时间点与时间点之间的关联，但他们的去诶按也十分明显。从1997年LSTM诞生以来，深度学习研究者依然在不断地寻找更为高效和创新的方法来处理这种时间序列数据。在2023年的今天，有大量精彩纷呈的时序数据处理思路以及算法，他们构建了深度学习时间序列算法群，其中非常引人注目的一脉算法就是卷积神经网络。\n",
    "\n",
    "卷积神经网络通常被认为是计算机视觉领域的核心算法，但事实上在今天，卷积已经被拓展到语言、时序、生成式算法等各个领域。得益于卷积独特的计算方式，卷积神经网络可以轻松处理较长的序列，而不会令算法差生遗忘问题，同时由于卷积可以并行、卷积层的参数更少，因此卷积网络在处理时序数据时，效率更高、更轻量，在许多时候也可以获得超越循环网络算法群的表现。今天，我们将深入挖掘卷积网络在处理时间序列数据方面的能力，并探讨其背后的原因。希望这次的探讨能够为大家带来新的启示，让我们更好地理解时序卷积网络TCN的魅力。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9b6ed303-fd9f-4613-af84-73363e9da36c",
   "metadata": {},
   "source": [
    "## 2 卷积运算流程与基本运行逻辑"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cf576b42-ccf8-41c7-801f-92301d259e0f",
   "metadata": {},
   "source": [
    "在了解卷积网络是如何处理时间序列之前，我们首先要了解如下如下事实——**卷积神经网络是使用多个卷积层构成的神经网络，卷积层是使用卷积运算的神经网络层**，在深度学习中，**卷积运算可以被理解成是一种按顺序对矩阵或序列进行点积的运算，而点积就是将矩阵/序列中的对应位置元素相乘相加、最终得到一个标量的运算**。让我们从点积开始理解。\n",
    "\n",
    "点积是两个尺寸一致、维度一致的向量或矩阵，进行对应位置元素相乘相加、最终得到一个标量的运算，来看下面的两个矩阵：\n",
    "\n",
    "![](https://skojiangdoc.oss-cn-beijing.aliyuncs.com/2021PyTorchDL/21OctOpenClass/23.PNG)\n",
    "\n",
    "一个是字母a-i（字母矩阵），另一个是数字1-9（数字矩阵），尺寸都是3x3。现在，我们求解两个矩阵的点积，则可以得到：\n",
    "\n",
    "**<center>点积 = $\\boldsymbol{ a + 2b + 3c + 4d + 5e + 6f + 7g + 8h + 9i}$</center>**"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ceaa9834-ce63-4db1-9526-c27dfd4713b0",
   "metadata": {},
   "source": [
    "对于两个序列，我们也可以计算点积，例如对序列[1, 2, 3]和序列[A,B,C]计算点积，则有：\n",
    "\n",
    "![01](https://skojiangdoc.oss-cn-beijing.aliyuncs.com/2023DL/1dcnn/01.png)\n",
    "\n",
    "**<center>点积 = $\\boldsymbol{ a + 2b + 3c}$</center>**"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d570823f-4cbe-490a-8789-38560632714b",
   "metadata": {},
   "source": [
    "卷积是按顺序对矩阵或序列进行点积的运算，那具体如何按顺序进行点积？虽然点积只能发生在尺寸维度相同的数据之间，但卷积却可以发生在尺寸不同的数据之间。如下所示，假设我们现在有一个10x2的矩阵和一个3x2的矩阵，我们可以让较小的矩阵让分别与大矩阵中的1-3行样本、2-4行样本、3-5行样本……直到8-10行样本做点积，仿佛在使用小矩阵**扫描**大矩阵一般，生成一串点积的结果。这样**从上至下、依据样本的顺序对矩阵进行扫描点积、并生成一个新序列的计算方式，就是一维卷积运算**。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cac109e2-570f-40c5-9a84-83c2082e6c2c",
   "metadata": {},
   "source": [
    "![02](https://skojiangdoc.oss-cn-beijing.aliyuncs.com/2023DL/1dcnn/05.png)\n",
    "\n",
    "![03](https://skojiangdoc.oss-cn-beijing.aliyuncs.com/2023DL/1dcnn/06.png)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "17399c84-4e52-4a91-a5ea-518c49098eb4",
   "metadata": {},
   "source": [
    "同样的，假设我们现在依然拥有大矩阵（尺寸为10x3），一个小矩阵（尺寸为2x2）。我们依然让小矩阵“扫描”大矩阵，但此时，我们并不只是从上至下进行扫描，而是让小矩阵从左到右、再从上到下进行扫描——图图所示，小矩阵分别与大矩阵中的1、2、11、12号样本、11、12、21、22号样本、2、3、12、13好号样本……直到19、20、29、30号样本做点积，生成一组点积的结果。这样**从左到右、同时从上至下进行点积、并生成一个新矩阵的计算方式，就是二维卷积运算**。\n",
    "\n",
    "![02](https://skojiangdoc.oss-cn-beijing.aliyuncs.com/2023DL/1dcnn/07.png)\n",
    "\n",
    "![03](https://skojiangdoc.oss-cn-beijing.aliyuncs.com/2023DL/1dcnn/08.png)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c24eb409-1ab2-47c8-973a-3dda1aa58030",
   "metadata": {},
   "source": [
    "很显然，一维卷积只按照样本顺序进行扫描，而二维卷积会按照特征的顺序、样本的顺序同时进行扫描。**扫描方向的不同，是一维卷积与二维卷积的根本区别**，由于扫描方向不同，**因此一维卷积只会生成序列，而二维卷积往往生成矩阵**。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "98294923-ad70-447f-b61f-d6f8955d3660",
   "metadata": {},
   "source": [
    "如果将上述流程类比到深度学习当中，那大矩阵就是我们需要处理的原始数据，小矩阵就是卷积神经网络根据算法使用者的需求生成的、专用于与原始数据进行点积运算的**卷积核**。卷积核的**长和宽**往往可以由算法使用者自定义，只不过在一维卷积当中，算法使用者只能定义卷积核的长度，一维卷积核的宽度一定是与原始数据的特征数量一致的；在二维卷积中，算法使用者既可以定义卷积核的长度、又可以定义卷积核的宽度，一般来说我们使用的是正方形的卷积核。\n",
    "\n",
    "卷积核的尺寸是算法使用者定义的，那卷积核中的数字从哪里来呢？事实上，这些数字是在神经网络实例化时随机生成的——你或许已经想到了，**卷积核中的数字就是卷积神经网络的权重**$w$，卷积神经网络的训练和迭代流程就是找到最适合当前原始数据的卷积核的流程。卷积运算的过程，本质就是特征提取、信息提炼的过程，因此只要找到适当的卷积核，就可以对信息进行最为有效的提取，从而能够进行有效的预测。\n",
    "\n",
    "**以上的流程可以完美地对应到卷积层当中**，大矩阵就是卷积层的输入，点积序列和矩阵就是卷积层的输出，而卷积核就是卷积层的权重。所以卷积层是输入矩阵，输出序列/矩阵的神经网络层。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "37593f11-b4f0-483b-8207-a1f2d31ab16e",
   "metadata": {},
   "source": [
    "- **卷积运算与时间序列有什么联系？卷积为什么很适合时间序列？**"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c13eaaba-fd2f-4ece-b085-fe35a7457399",
   "metadata": {},
   "source": [
    "现在我们已经知道卷积层是如何运作的了，但是这与时间序列有什么关系呢？在深度学习的世界中，一维卷积和二维卷积都可以被用于时间序列的处理，不过一维卷积用于时间序列的时候更多，我们来看看具体如何操作——在时间序列的数据展示方式中，最为简单的就是单变量时序数据，这是只有时间点作为索引、没有特征、只有标签的数据类型——\n",
    "\n",
    "|时间|股价|\n",
    "|:-:|:-:|\n",
    "|9：00|xxx|\n",
    "|9：01|xxx|\n",
    "|9：02|xxx|\n",
    "|9：03|xxx|\n",
    "|9：04|xxx|\n",
    "|9：05|xxx|\n",
    "|……||\n",
    "|9：56|xxx|\n",
    "|9：57|xxx|\n",
    "|9：58|xxx|\n",
    "\n",
    "或者——\n",
    "\n",
    "|日期|摄氏度|\n",
    "|:-:|:-:|\n",
    "|6月1日|xxx|\n",
    "|6月2日|xxx|\n",
    "|6月3日|xxx|\n",
    "|6月4日|xxx|\n",
    "|6月5日|xxx|\n",
    "|6月6日|xxx|\n",
    "|……||\n",
    "|6月28日|xxx|\n",
    "|6月29日|xxx|\n",
    "|6月30日|xxx|\n",
    "\n",
    "单变量时间序列就是一列从上向下按时间顺序排列的数字。我们可以在这串数字上进行一维卷积运算，以提取原始时间序列的信息、生成新的序列。此时，由于输入的数据是一维的，所以卷积核也是一维的（宽为1），我们将可以按照如下的方式处理时间序列数据：\n",
    "\n",
    "![02](https://skojiangdoc.oss-cn-beijing.aliyuncs.com/2023DL/1dcnn/02.png)\n",
    "\n",
    "在深度学习当中，大部分的序列数据是多变量的——\n",
    "\n",
    "|时间|开盘价|收盘价|交易量|……|波动率|\n",
    "|:-:|:-:|:-:|:-:|:-:|:-:|\n",
    "|9：00|xxx|xxx|xxx|xxx|xxx|\n",
    "|9：01|xxx|xxx|xxx|xxx|xxx|\n",
    "|9：02|xxx|xxx|xxx|xxx|xxx|\n",
    "|9：03|xxx|xxx|xxx|xxx|xxx|\n",
    "|9：04|xxx|xxx|xxx|xxx|xxx|\n",
    "|9：05|xxx|xxx|xxx|xxx|xxx|\n",
    "|……||||||\n",
    "|9：56|xxx|xxx|xxx|xxx|xxx|\n",
    "|9：57|xxx|xxx|xxx|xxx|xxx|\n",
    "|9：58|xxx|xxx|xxx|xxx|xxx|\n",
    "\n",
    "那此时我们可以使用宽度=特征数量的卷积核来对所有的特征进行并行处理。如下图所示，面对3个特征的时间序列/文字序列矩阵，我们可以使用结构为（自定义长度,3）的卷积核对序列进行扫描。此时，**一个卷积核会覆盖到的数据范围不止跨越时间点，还跨越特征值，因此卷积可以同时捕获到样本与样本之间、特征与样本之间、特征与特征之间、以及所有信息与标签之间的联系**。这是一种效率极高的方式，通过并行的运算，就可以一次性捕获所有相关的信息。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "531b813c-bd43-4c6d-a2b8-be76275283c7",
   "metadata": {},
   "source": [
    "![04](https://skojiangdoc.oss-cn-beijing.aliyuncs.com/2023DL/1dcnn/04.gif)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "de69ed0f-5341-47e7-aba6-500fef617b6b",
   "metadata": {},
   "source": [
    "当然，面对更高维度的时间序列数据，例如最常见的三维时间序列——\n",
    "\n",
    "![](http://skojiangdoc.oss-cn-beijing.aliyuncs.com/2023DL/Live/NLP%26LLMs/03_.png)\n",
    "\n",
    "卷积不止能够在特征上并行，还能够同时对多张时间序列表单进行处理，假设现在有红、绿、蓝三张时间序列表单，则我们可以一次性扫描掉所有表单的数据——"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a96936ff-35e6-4d27-8f11-667c7b4d5d3c",
   "metadata": {},
   "source": [
    "![](https://skojiangdoc.oss-cn-beijing.aliyuncs.com/2023DL/1dcnn/09.gif)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bc43470a-5beb-4004-8179-84e4e32cf04f",
   "metadata": {},
   "source": [
    "因此，面对结构为(batch_size, time_step, input_dimension)的三维时间序列数据，卷积可以一次性扫描掉这个batch中所有的表单，从而达到超高效并行的目的。现在你知道卷积网络是如何在时间序列数据上运行的了，**那卷积网络在时间序列上运行，有何独特优势呢**？\n",
    "\n",
    "> **1. 丰富的并行机制，大幅提升模型的效率**\n",
    "\n",
    "在循环神经网络中，我们必须依次对样本进行正向传播，但是在卷积网络中，我们可以一次性对多个样本进行卷积。最夸张的情况下，我们可以使卷积核的长度就等同于时间步的数量，一次卷积运算即可获得全部样本上的信息，这个效率是大幅提升。除此之外，卷积可以在不同的时间序列表单上并行，这使得卷积网络的效率天生会高于循环类网络。\n",
    "\n",
    "> **2. 可以对同一套数据进行不同的解读**\n",
    "\n",
    "在卷积网络中，一次扫描就相当于一次对信息的处理和解读，**我们可以创建多个卷积核、对同一组数据进行多次的扫描**，以逼迫算法以不同的方式来解读同样的数据——\n",
    "\n",
    "![](http://skojiangdoc.oss-cn-beijing.aliyuncs.com/2023DL/1dcnn/12.png)\n",
    "\n",
    "有几个卷积核、进行几次扫描，就可以在一维卷积中生成几个序列。所以一维卷积的输出究竟有多少列，是由人为规定的。对同样的数据进行不同的解读，有助于算法从不同的角度去提取信息，并且幸运的是，**使用不同的卷积核对数据进行扫描这一事项也是可以并行的**，因此增加扫描的次数并不会大幅降低卷积神经网络的运算效率。\n",
    "\n",
    "> **3. 卷积天然可以获取多个样本上的信息**\n",
    "\n",
    "卷积运算可以一次性扫描到多个时间点，因此卷积运算天生就能够非常巧妙地提取到时间点与时间点之间的联系，因为**当一个卷积核进行卷积运算后，新生成的点积序列中的每个值都会包含不止一个时间点的信息**，这就让卷积运算天生拥有“整合不同时间点之间信息”的能力，正适合于时间序列预测中“必须探索时间点与时间点之间联系”的根本需求。\n",
    "\n",
    "![02](https://skojiangdoc.oss-cn-beijing.aliyuncs.com/2023DL/1dcnn/13.png)\n",
    "\n",
    "但除此之外，卷积网络可以通过叠加卷积层的方式，让一个点积结果中包含非常长的序列上的信息。当一个卷积层输出了点积矩阵后，这个点积矩阵可以作为下一个卷积层的输入，我们可以重新生成卷积核、在上个卷积层生成的点击矩阵上进行扫描。当第二个卷积核完成运算，输出心的点积矩阵后，这个\n",
    "点积矩阵上的样本将大幅包含原始序列上的信息，如下图所示——\n",
    "\n",
    "![02](https://skojiangdoc.oss-cn-beijing.aliyuncs.com/2023DL/1dcnn/14.png)\n",
    "\n",
    "一个点积矩阵上的值能够映射到原始矩阵上的范围被称之为是“当前卷积层的感受野”，感受野越大，一个点积值能够包含的时间序列就越长。可以看到，卷积网络可以轻松通过堆叠卷积层的方式来让算法拥有“长时间、长距离视野”，从而让算法拥有能够处理长序列的能力。这一点是循环网络望其项背的。\n",
    "\n",
    "由于卷积的数学流程可以轻松在不同时间点的信息之间建立联系，同时卷积还能够实现超高效的并行策略，因此卷积神经网络可以处理任意序列数据，但是要处理时间序列数据还需要更多的改进和修饰。在TCN当中，我们主要使用的是一维卷积，同时TCN的作者还对一维卷积进行了改进，让我们一起来看看TCN当中的卷积层究竟是什么样的。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3cb43a52-f9da-4be1-90ae-df488e60c274",
   "metadata": {},
   "source": [
    "![](https://skojiangdoc.oss-cn-beijing.aliyuncs.com/2023DL/LSTM/%E8%AF%BE%E7%A8%8B%E5%AE%A3%E4%BC%A011.jpg)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cc3a3d37-2d60-4da8-9e70-a7056f9406c4",
   "metadata": {
    "jupyter": {
     "source_hidden": true
    },
    "tags": []
   },
   "source": [
    "![](https://skojiangdoc.oss-cn-beijing.aliyuncs.com/2023DL/LSTM/%E8%AF%BE%E7%A8%8B%E5%AE%A3%E4%BC%A05.png)\n",
    "\n",
    "![](https://skojiangdoc.oss-cn-beijing.aliyuncs.com/2023DL/LSTM/%E8%AF%BE%E7%A8%8B%E5%AE%A3%E4%BC%A06.png)\n",
    "\n",
    "![](https://skojiangdoc.oss-cn-beijing.aliyuncs.com/2023DL/LSTM/%E8%AF%BE%E7%A8%8B%E5%AE%A3%E4%BC%A09.png)\n",
    "\n",
    "|||\n",
    "|:-:|:-:|\n",
    "|![](https://skojiangdoc.oss-cn-beijing.aliyuncs.com/2023DL/LSTM/%E8%AF%BE%E7%A8%8B%E5%AE%A3%E4%BC%A01.png)|![](https://skojiangdoc.oss-cn-beijing.aliyuncs.com/2023DL/LSTM/%E8%AF%BE%E7%A8%8B%E5%AE%A3%E4%BC%A07.png)|"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4db15594-87b7-4894-9253-e0068a417956",
   "metadata": {
    "tags": []
   },
   "source": [
    "## 3 TCN中的卷积层与基本元件"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "50b4abea-914e-4ec8-a465-629339276b54",
   "metadata": {},
   "source": [
    "在TCN当中，最为核心的计算结构被称之为膨胀因果卷积，它由膨胀卷积和因果卷积两种卷积构成，我们来看一下——"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "41180289-2f27-4fef-8095-b37254f83edc",
   "metadata": {},
   "source": [
    "- **因果卷积**"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8b610744-5421-4027-bf65-7d56a2428333",
   "metadata": {},
   "source": [
    "在深度学习和信号处理中，卷积是一个常见的操作。但在处理时间序列数据时，特别是在预测未来的时刻，我们不希望未来的信息在当前时刻被使用。但是在一维卷积的计算过程中，很显然我们会涉及到的不止过去的时间点，还有未来的时间点（即不止会涉及到上方的样本，还会涉及到下方的样本）——"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cc0f4732-422c-4591-bf24-c8d776df9a70",
   "metadata": {},
   "source": [
    "![02](https://skojiangdoc.oss-cn-beijing.aliyuncs.com/2023DL/1dcnn/02.png)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "403ec5d6-efea-4492-960d-a147ee5d4fe3",
   "metadata": {},
   "source": [
    "这就引入了所谓的“因果卷积”。因果卷积保证了在任何时间点t，输出只依赖于时间点t及其之前的输入，而不依赖于t之后的输入。因果卷积可以通过对输入数据进行适当的“填充”来实现。具体地说，假设我们有一个1D的输入序列和一个大小为k的卷积核，为了实现因果卷积，我们可以在序列的开始处填充k-1个零，然后进行标准的卷积操作。这样，卷积的输出在任何时间点t都只会依赖于时间点t及其之前的输入，如下图所示："
   ]
  },
  {
   "cell_type": "markdown",
   "id": "098c0279-dbdb-4b61-b3c6-ace77a3ffbfc",
   "metadata": {},
   "source": [
    "![02](https://skojiangdoc.oss-cn-beijing.aliyuncs.com/2023DL/1dcnn/10.png)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "75564e2d-3941-43a7-b630-c3706b0211cd",
   "metadata": {},
   "source": [
    "- **膨胀卷积**"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0d75ea41-30e8-4548-85eb-dffd602e0a26",
   "metadata": {},
   "source": [
    "膨胀卷积（Dilated Convolution）是TCN（Temporal Convolutional Network）中的关键组件，它可以通过对卷积核填上“空洞”的方式来放大卷积层的感受野。填补空洞的方式是卷及操作中常见的方式，这种方式无需增加模型参数或计算成本，就可以轻松放大感受野，我们来看看具体是如何操作。\n",
    "\n",
    "在标准的卷积中，卷积核的元素是连续的，一次覆盖输入数据的连续部分。而在膨胀卷积中，卷积核的元素之间存在间隔，这些间隔使得卷积核可以覆盖更广的范围。如下所示，当我们使用膨胀指数为1时，就是在原始卷积核的每行中填补一行0——"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3dbb8b44-b9ee-4caa-ba5b-ce82abcfb9c2",
   "metadata": {},
   "source": [
    "![02](https://skojiangdoc.oss-cn-beijing.aliyuncs.com/2023DL/1dcnn/15.png)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "90a58ed8-fd07-4af2-bb23-0935c4a4db41",
   "metadata": {},
   "source": [
    "膨胀卷积可以很大程度帮助我们放大感受野，当卷积层堆叠的时候——"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5b2f55e1-b28e-4aab-9d23-73354dd995b1",
   "metadata": {},
   "source": [
    "![02](https://skojiangdoc.oss-cn-beijing.aliyuncs.com/2023DL/1dcnn/16.png)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "31ee9be7-9e43-4eaf-ac0c-040e4dd34809",
   "metadata": {},
   "source": [
    "很显然，如果我们使用更大的膨胀指数，那感受野就可以被放得更大。在TCN当中，原作者建议的膨胀指数是第一个卷积层使用1，第二个卷积层使用2，第三个卷积层使用4，这种结构可以使网络的顶层捕捉到非常长的时间依赖关系，而底层则可以捕捉到更短的依赖关系。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2db7a808-6412-4a65-b8e1-69ee77be73e7",
   "metadata": {},
   "source": [
    "- **膨胀因果卷积与残差链接**"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cc327d03-9f30-42c7-84e2-b48935538f00",
   "metadata": {},
   "source": [
    "残差连接（或残差块、残差链接）是深度学习中一种增强网络训练稳定性的技术。它首次由He等人在2015年的文章中提出，用于解决深层网络中的梯度消失和梯度爆炸问题。这种设计后来在多种网络架构中被广泛采用，包括TCN（Temporal Convolutional Network）。\n",
    "\n",
    "在TCN中，残差链接的主要目的是帮助模型学习不同时间尺度上的依赖关系，并确保深度增加时的训练稳定性。在残差连接的设计中，当前层的输出不仅传递给下一层，而且与输入直接相加，从而形成一个“短路”连接。这种设计允许信息直接流过多个层，提供了一种更直接的路径更新梯度。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0537a385-14eb-4f7d-b02f-db5936e716f3",
   "metadata": {},
   "source": [
    "![](https://skojiangdoc.oss-cn-beijing.aliyuncs.com/2023DL/1dcnn/17.png)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "62d975df-3979-47e6-8855-8dbdc9f7de1c",
   "metadata": {},
   "source": [
    "在TCN中，残差链接构建了残差块，而残差快由以下路径组成：\n",
    "\n",
    "**主路径**：输入首先通过一系列操作，例如卷积、批量归一化和激活函数。\n",
    "\n",
    "**残差路径**：输入不经过任何操作直接进行。\n",
    "\n",
    "**融合**：主路径和残差路径的输出在深度方向上相加，形成最终的输出。\n",
    "\n",
    "这种结构确保了，如果模型认为当前层的操作不会为最终的输出增加任何有益的信息，那么它可以将这些操作的权重设置得很小，从而使主路径的输出接近于零。这样，模型的输出就主要依赖于残差连接。这给予模型一种选择，要么使用主路径，要么依赖残差路径。在实际实现中，为了确保主路径的输出和残差路径的输出具有相同的形状，可能需要对输入或输出进行某种修改。例如，如果主路径中的卷积操作改变了特征的数量，那么可能需要在残差路径中添加一个1x1的卷积来匹配特征数量。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "28b94793-dd7c-498a-84b4-b3230ef28832",
   "metadata": {},
   "source": [
    "## 4 一维卷积的参数与TCN的实现"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "316e8490-82b6-478c-acd5-519376f84324",
   "metadata": {},
   "source": [
    "在之前的课程当中，我们已经认识了PyTorch框架的基本结构，整个PyTorch框架可以大致被分Torch和成熟AI领域两大板块，其中Torch包含各类神经网络组成元素、用于构建各类神经网络，各类AI领域中则包括Torchvision、Torchtext、Torchaudio等辅助完成图像、文字、语音方面各类任务的领域模块。\n",
    "\n",
    "在PyTorch中，LSTM是属于“构建循环神经网络的元素”，而非“成熟神经网络”，因此LSTM是位于PyTorch.nn这个基本模块下。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f373c5e2-5d12-41d6-8b5f-742ba8af2dd8",
   "metadata": {},
   "source": [
    "![](https://skojiangdoc.oss-cn-beijing.aliyuncs.com/2023DL/Live/NLP%26LLMs/24.png)\n",
    "\n",
    "![](http://skojiangdoc.oss-cn-beijing.aliyuncs.com/2023DL/Live/NLP%26LLMs/25.png)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5b6e43fc-8d8c-4d6a-82c3-0d672508d561",
   "metadata": {},
   "source": [
    "来看看位于torch.nn模块下的Conv1d层："
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b7e4b5c3-abe8-4c05-ab71-8b427a2d1d86",
   "metadata": {},
   "source": [
    "`torch.nn.Conv1d(in_channels, out_channels, kernel_size, stride=1, padding=0, dilation=1, groups=1, bias=True, padding_mode='zeros', device=None, dtype=None)`"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c5eb93d3-fefa-46fa-b069-2b57b6209ac6",
   "metadata": {},
   "source": [
    "如果你熟悉计算机视觉，那你看向一维卷积的类时一定会感觉到非常熟悉。在PyTorch当中，一维卷积层和二维卷积层的参数是100%一致的，让我们来看看这些重要参数："
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bd15684f-252f-41dc-8e67-372a436ba36e",
   "metadata": {},
   "source": [
    "**in_channels**: 输入数据的通道数。例如，如果你的输入数据是一个具有多个特征的时间序列，那么in_channels就等于这些特征的数量。\n",
    "\n",
    "**out_channels**: 输出数据的列数。这等于卷积运算执行的次数，也就是卷积核的数量。\n",
    "\n",
    "**kernel_size**:  卷积核的大小。在一维卷积中，这是一个单一的整数，表示卷积核覆盖的连续数据点数。\n",
    "\n",
    "**stride** (默认为 1):卷积操作时卷积核移动的步长。如果步长大于1，输出的尺寸会减小。\n",
    "\n",
    "**padding** (默认为 0): 在输入数据的两侧加上的零的数量。填充可以帮助控制输出的尺寸，特别是当你希望输出尺寸与输入尺寸相同时。\n",
    "\n",
    "**dilation** (默认为 1): 膨胀卷积中的膨胀指数。当膨胀率大于1时，卷积核中的元素之间会有间隔，从而增大了卷积的感受野。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "9bdcf531-30de-4ea7-8c28-fe8c82a18442",
   "metadata": {},
   "outputs": [],
   "source": [
    "from torch import nn\n",
    "import torch"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 39,
   "id": "64ba1dec-2215-4493-b921-c61a7ea11fc0",
   "metadata": {},
   "outputs": [],
   "source": [
    "m = nn.Conv1d(in_channels=16, out_channels=1, kernel_size=3, padding=1)\n",
    "#输入的维度是三维，注意在PyTorch中，实际的维度是（batch_size，input_dimension，time_step）\n",
    "input = torch.randn(20, 16, 50)\n",
    "output = m(input)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 40,
   "id": "06aa1781-890e-48ca-b059-44086e1b3928",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "torch.Size([20, 1, 50])"
      ]
     },
     "execution_count": 40,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "output.shape #输出的维度也是三维"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 41,
   "id": "8e42f16e-927f-4412-a47e-70ea322d281d",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "torch.Size([1, 16, 3])"
      ]
     },
     "execution_count": 41,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "m.weight.shape #这就是卷积核的大小"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "270c39dc-de35-4c22-9a15-fa3dd9d08fc9",
   "metadata": {},
   "source": [
    "基于一维卷积，我们来实现TCN的基本结构："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 45,
   "id": "63c80a8f-9db7-4828-bffc-9434ae11edc1",
   "metadata": {},
   "outputs": [],
   "source": [
    "import torch\n",
    "import torch.nn as nn\n",
    "from torch.nn.utils import weight_norm"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 46,
   "id": "e02e07c3-8e2c-4b6f-84f2-cd473bd53c00",
   "metadata": {},
   "outputs": [],
   "source": [
    "class Chomp1d(nn.Module):\n",
    "    def __init__(self, chomp_size):\n",
    "        super(Chomp1d, self).__init__()\n",
    "        self.chomp_size = chomp_size\n",
    "\n",
    "    def forward(self, x):\n",
    "        \"\"\"\n",
    "        其实这就是一个裁剪的模块，裁剪多出来的padding\n",
    "        \"\"\"\n",
    "        return x[:, :, :-self.chomp_size].contiguous()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 47,
   "id": "99c4dd86-105f-452a-a43d-36128e1d938f",
   "metadata": {},
   "outputs": [],
   "source": [
    "class TemporalBlock(nn.Module):\n",
    "    def __init__(self, n_inputs, n_outputs, kernel_size, stride, dilation, padding, dropout=0.2):\n",
    "        \"\"\"\n",
    "        相当于一个Residual block\n",
    "\n",
    "        :param n_inputs: int, 输入通道数\n",
    "        :param n_outputs: int, 输出通道数\n",
    "        :param kernel_size: int, 卷积核尺寸\n",
    "        :param stride: int, 步长，一般为1\n",
    "        :param dilation: int, 膨胀系数\n",
    "        :param padding: int, 填充系数\n",
    "        :param dropout: float, dropout比率\n",
    "        \"\"\"\n",
    "        super(TemporalBlock, self).__init__()\n",
    "        self.conv1 = weight_norm(nn.Conv1d(n_inputs, n_outputs, kernel_size,\n",
    "                                           stride=stride, padding=padding, dilation=dilation))\n",
    "        # 经过conv1，输出的size其实是(Batch, input_channel, seq_len + padding)\n",
    "        self.chomp1 = Chomp1d(padding)  # 裁剪掉多出来的padding部分，维持输出时间步为seq_len\n",
    "        self.relu1 = nn.ReLU()\n",
    "        self.dropout1 = nn.Dropout(dropout)\n",
    "\n",
    "        self.conv2 = weight_norm(nn.Conv1d(n_outputs, n_outputs, kernel_size,\n",
    "                                           stride=stride, padding=padding, dilation=dilation))\n",
    "        self.chomp2 = Chomp1d(padding)  #  裁剪掉多出来的padding部分，维持输出时间步为seq_len\n",
    "        self.relu2 = nn.ReLU()\n",
    "        self.dropout2 = nn.Dropout(dropout)\n",
    "\n",
    "        self.net = nn.Sequential(self.conv1, self.chomp1, self.relu1, self.dropout1,\n",
    "                                 self.conv2, self.chomp2, self.relu2, self.dropout2)\n",
    "        self.downsample = nn.Conv1d(n_inputs, n_outputs, 1) if n_inputs != n_outputs else None\n",
    "        self.relu = nn.ReLU()\n",
    "        self.init_weights()\n",
    "\n",
    "    def init_weights(self):\n",
    "        \"\"\"\n",
    "        参数初始化\n",
    "\n",
    "        :return:\n",
    "        \"\"\"\n",
    "        self.conv1.weight.data.normal_(0, 0.01)\n",
    "        self.conv2.weight.data.normal_(0, 0.01)\n",
    "        if self.downsample is not None:\n",
    "            self.downsample.weight.data.normal_(0, 0.01)\n",
    "\n",
    "    def forward(self, x):\n",
    "        \"\"\"\n",
    "        :param x: size of (Batch, input_channel, seq_len)\n",
    "        :return:\n",
    "        \"\"\"\n",
    "        out = self.net(x)\n",
    "        res = x if self.downsample is None else self.downsample(x)\n",
    "        return self.relu(out + res)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 44,
   "id": "08f264c0-af66-4182-95a1-f2fd472de35b",
   "metadata": {},
   "outputs": [],
   "source": [
    "class TemporalConvNet(nn.Module):\n",
    "    def __init__(self, num_inputs, num_channels, kernel_size=2, dropout=0.2):\n",
    "        \"\"\"\n",
    "        TCN，目前paper给出的TCN结构很好的支持每个时刻为一个数的情况，即sequence结构，\n",
    "        对于每个时刻为一个向量这种一维结构，勉强可以把向量拆成若干该时刻的输入通道，\n",
    "        对于每个时刻为一个矩阵或更高维图像的情况，就不太好办。\n",
    "\n",
    "        :param num_inputs: int， 输入通道数\n",
    "        :param num_channels: list，每层的hidden_channel数，例如[25,25,25,25]表示有4个隐层，每层hidden_channel数为25\n",
    "        :param kernel_size: int, 卷积核尺寸\n",
    "        :param dropout: float, drop_out比率\n",
    "        \"\"\"\n",
    "        super(TemporalConvNet, self).__init__()\n",
    "        layers = []\n",
    "        num_levels = len(num_channels)\n",
    "        for i in range(num_levels):\n",
    "            dilation_size = 2 ** i   # 膨胀系数：1，2，4，8……\n",
    "            in_channels = num_inputs if i == 0 else num_channels[i-1]  # 确定每一层的输入通道数\n",
    "            out_channels = num_channels[i]  # 确定每一层的输出通道数\n",
    "            layers += [TemporalBlock(in_channels, out_channels, kernel_size, stride=1, dilation=dilation_size,\n",
    "                                     padding=(kernel_size-1) * dilation_size, dropout=dropout)]\n",
    "\n",
    "        self.network = nn.Sequential(*layers)\n",
    "\n",
    "    def forward(self, x):\n",
    "        \"\"\"\n",
    "        输入x的结构不同于RNN，一般RNN的size为(Batch, seq_len, channels)或者(seq_len, Batch, channels)，\n",
    "        这里把seq_len放在channels后面，把所有时间步的数据拼起来，当做Conv1d的输入尺寸，实现卷积跨时间步的操作，\n",
    "        很巧妙的设计。\n",
    "        \n",
    "        :param x: size of (Batch, input_channel, seq_len)\n",
    "        :return: size of (Batch, output_channel, seq_len)\n",
    "        \"\"\"\n",
    "        return self.network(x)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5f64baa8-d162-4b3e-a33e-ee1e320fbff7",
   "metadata": {},
   "source": [
    "![](https://skojiangdoc.oss-cn-beijing.aliyuncs.com/2023DL/LSTM/%E8%AF%BE%E7%A8%8B%E5%AE%A3%E4%BC%A011.jpg)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6883e596-3590-4bb2-9795-ff918e02a494",
   "metadata": {
    "jupyter": {
     "source_hidden": true
    },
    "tags": []
   },
   "source": [
    "![](https://skojiangdoc.oss-cn-beijing.aliyuncs.com/2023DL/LSTM/%E8%AF%BE%E7%A8%8B%E5%AE%A3%E4%BC%A05.png)\n",
    "\n",
    "![](https://skojiangdoc.oss-cn-beijing.aliyuncs.com/2023DL/LSTM/%E8%AF%BE%E7%A8%8B%E5%AE%A3%E4%BC%A06.png)\n",
    "\n",
    "![](https://skojiangdoc.oss-cn-beijing.aliyuncs.com/2023DL/LSTM/%E8%AF%BE%E7%A8%8B%E5%AE%A3%E4%BC%A09.png)\n",
    "\n",
    "|||\n",
    "|:-:|:-:|\n",
    "|![](https://skojiangdoc.oss-cn-beijing.aliyuncs.com/2023DL/LSTM/%E8%AF%BE%E7%A8%8B%E5%AE%A3%E4%BC%A01.png)|![](https://skojiangdoc.oss-cn-beijing.aliyuncs.com/2023DL/LSTM/%E8%AF%BE%E7%A8%8B%E5%AE%A3%E4%BC%A07.png)|"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ea69622b-1712-4b98-917b-78f2e4ca23bc",
   "metadata": {},
   "source": [
    "# 【公开课周五继续】"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "89ca1736-8aa0-4709-a3b8-c8077e717c3b",
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.9.7"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
